package javax.per.toolkit.file;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.per.toolkit.tools.Log;
import javax.per.toolkit.tools.Log.LogLevel;

import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
import org.apache.tools.zip.ZipOutputStream;

/**
 * 
 * @author yl34569
 * 
 */
public class ZipUtil {
	/**
	 * File list to be compressed.
	 */
	private List<File> fileList = new ArrayList<File>();

	public List<File> getFileList() {
		return fileList;
	}

	public void clearFileList() {
		this.fileList = null;
	}

	/**
	 * The target path of the compressed file.
	 */
	private String targetPath = null;

	private String relativePath = null;

	/**
	 * Determines if to show the detial information.
	 */

	/**
	 * Determines if to cover exist files,if not may throw exception.
	 */
	private boolean coverExistFile = false;

	/**
	 * Setting if to cover exist files
	 * 
	 * @param b
	 *            true stands for cover the exist files.
	 * @return the instance of the ZipUtil class.
	 */
	public ZipUtil coverExisting(boolean b) {
		this.coverExistFile = b;
		return this;
	}

	/**
	 * Set the debug flag.
	 * 
	 * @param b
	 *            debug flag.
	 * @return the instance of the ZipUtil class.
	 */
	public ZipUtil setDebug(boolean b) {
		if(b)
			Log.setLevel(LogLevel.DEBUG);
		else
			Log.setLevel(LogLevel.OFF);
		return this;
	}

	/**
	 * Put file into the list to be compressed.
	 * 
	 * @param filePath
	 *            the path of the file.
	 * @return the instance of the ZipUtil class.
	 */
	public ZipUtil putFile(String filePath) {
		File f = new File(filePath);
		if (!f.exists())
			throw new Error("File \"" + filePath + "\" is not exist");
		fileList.add(f);
		return this;
	}

	/**
	 * Put file into the list to be compressed using a directory.
	 * 
	 * @param dirName
	 *            the directory name.
	 * @param suffix
	 *            the suffix of file to be add in the directory.
	 * @return the instance of this class.
	 */
	public ZipUtil putFileDir(String dirName, String suffix) {
		File f = new File(dirName);
		if (!f.exists())
			throw new Error("File not found");
		if (!f.isDirectory())
			throw new Error("File specialized is not a directory");
		File[] fs = f.listFiles();
		if (fs != null) {
			for (File fi : fs) {
				int pos = fi.getName().lastIndexOf(".");
				if (pos < 0 || pos == fi.getName().length())
					continue;
				String suffixTmp = fi.getName().substring(pos + 1);
				if (suffixTmp.equals(suffix)) {
					this.fileList.add(fi);
				}
			}
		}
		{
			for (File s : this.fileList)
				Log.getLogger().debug("Put File: " + s.getName());
		}
		return this;
	}
	
	

	/**
	 * Set the output path of the compressed file.
	 * 
	 * @param savePath
	 *            the output path
	 * @return the instance of the ZipUtil class.
	 */
	public ZipUtil setTargetPath(String savePath) {
		this.targetPath = savePath;
		return this;
	}

	/**
	 * Checks the target path.
	 */
	protected final void checkTargetPath() {
		if (this.targetPath == null)
			throw new Error("Target path not set");
		File f = new File(this.targetPath);
		String name = f.getName();
		if (name.indexOf(".") == -1) {
			this.targetPath += ".zip";
			f = new File(this.targetPath);
		}
		if (f.exists()) {
			if (!coverExistFile) {
				Log.getLogger().debug(f.getAbsolutePath());
				throw new Error("Target file already exist");
			} else
				f.delete();
		}
		if (!f.exists() || f.isDirectory()) {
			f.getParentFile().mkdirs();
			try {
				f.createNewFile();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Compress the files.
	 * 
	 * @throws IOException
	 */
	public void zipFiles() throws IOException {
		this.checkTargetPath();
		ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(
				this.targetPath));
		zos.setEncoding(System.getProperty("sun.jnu.encoding"));
		Iterator<File> it = fileList.iterator();
		while (it.hasNext()) {
			File tmp = it.next();
			relativePath = "";
			this.zipLoop(zos, tmp);
		}
		zos.flush();
		zos.close();

		Log.getLogger().debug("Zip completion");
	}

	public void zipFilesWithKey(String key) throws Exception {
		String targetPre = this.targetPath;
		String tmpName = UUID.randomUUID().toString();
		File tmp = File.createTempFile(tmpName, ".tmp");
		String targetNow = tmp.getAbsolutePath();
		tmp.delete();
		this.setTargetPath(targetNow);
		this.zipFiles();
		this.setTargetPath(targetPre);
		FileUtil.encryptFile(targetNow, targetPre, key);
		new File(targetNow).delete();

		Log.getLogger().debug("Encrypt completion");
	}

	/**
	 * Uncompress the files.
	 * 
	 * @param srcPath
	 *            the compressed file.
	 * @param desPath
	 *            the path of the uncompressed file to be placed.
	 * @throws IOException
	 */
	@SuppressWarnings("rawtypes")
	private void unzipFile(String srcPath, String desPath,
			boolean createZipSameNameDir) throws IOException {
		if (createZipSameNameDir) {
			if (new File(srcPath).getName().lastIndexOf(".") < 0)
				desPath += "/" + new File(srcPath).getName();
			else
				desPath += "/"
						+ new File(srcPath).getName().substring(0,
								new File(srcPath).getName().lastIndexOf("."));
		}
		desPath = desPath.replaceAll("\\\\", "/");
		File des = new File(desPath);
		File src = new File(srcPath);
		if (!des.isDirectory()) {
			des.mkdirs();
		}
		if (!src.exists()) {
			throw new Error("Src file not found");
		}
		ZipFile zipfile = new ZipFile(src,
				System.getProperty("sun.jnu.encoding"));
		Enumeration enu = zipfile.getEntries();
		if (enu == null)
			return;
		while (enu.hasMoreElements()) {
			ZipEntry ze = (ZipEntry) enu.nextElement();
			String zeName = ze.getName();
			zeName = zeName.replaceAll("\\\\", "/");
			if (zeName.endsWith("/")) {
				if (new File(desPath + File.separator + zeName).exists())
					new File(desPath + File.separator + zeName).delete();
				new File(desPath + File.separator + zeName).mkdirs();
			} else {
				this.buildFile(new File(desPath + File.separator + zeName));
				if (ze.getSize() > 0) {
					OutputStream os = null;
					try {
						os = new BufferedOutputStream(new FileOutputStream(
								new File(desPath + File.separator + zeName)));
						InputStream is = zipfile.getInputStream(ze);
						byte[] b = new byte[1 << 10];
						int length = 0;
						while ((length = is.read(b)) != -1) {
							os.write(b, 0, length);
						}
						is.close();
						os.close();
					} catch (Exception e) {

					}
				}
			}
		}

	}

	public void unzipFiles() throws IOException {
		if (this.targetPath == null) {
			throw new Error("Target path is not set.");
		}
		new File(this.targetPath).mkdirs();
		if (this.fileList == null) {
			throw new Error("File is not set.");
		}
		if (this.fileList.size() == 1) {
			for (int i = 0; i < this.fileList.size(); i++) {
				this.unzipFile(this.fileList.get(i).getAbsolutePath(),
						this.targetPath, false);
			}
		} else {
			for (int i = 0; i < this.fileList.size(); i++) {
				this.unzipFile(this.fileList.get(i).getAbsolutePath(),
						this.targetPath, true);

				Log.getLogger().debug(
						"Unzip: " + this.fileList.get(i).getAbsolutePath());
			}
		}

		Log.getLogger().debug("Unzip completion");
	}

	/**
	 * Uncompress the files.
	 * 
	 * @param srcPath
	 *            the compressed file.
	 * @param desPath
	 *            the path of the uncompressed file to be placed.
	 * @throws Exception
	 */
	public void unzipFileWithKey(String srcPath, String desPath, String key)
			throws Exception {
		String tmpName = UUID.randomUUID().toString();
		File tmp = File.createTempFile(tmpName, ".tmp");
		String targetNow = tmp.getAbsolutePath();
		tmp.delete();
		FileUtil.decryptFile(srcPath, targetNow, key);
		this.unzipFile(targetNow, desPath, false);
		new File(targetNow).delete();

		Log.getLogger().debug("Decrypt completion");
	}

	/**
	 * Find a file in some ZIP files.
	 * 
	 * @param fileName
	 *            the name of a file to be found.
	 * @return ZIP file list that contain the given file.
	 * @throws IOException
	 */
	public List<File> findFileInZips(String fileName) throws IOException {
		Vector<File> list = new Vector<File>();
		ExecutorService ser=Executors.newFixedThreadPool(3);
		for (File s : this.fileList) {
			ser.submit(new RunFindFileInJar(s,list,fileName));
		}
		ser.shutdown();
		while(!ser.isTerminated()){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		return list;
	}
	
	class RunFindFileInJar implements Runnable{
		File s;
		Vector<File> list;
		String fileName;
		public RunFindFileInJar(File f,Vector<File> list,String fileName){
			this.s=f;
			this.list=list;
			this.fileName=fileName;
		}

		public void run() {
			Log.getLogger().debug("Scan: " + s.getName());
			ZipFile zipfile = null;
			try {
				zipfile = new ZipFile(s,
						System.getProperty("sun.jnu.encoding"));
			} catch (IOException e) {
				e.printStackTrace();
			}
			@SuppressWarnings("rawtypes")
			Enumeration enu = zipfile.getEntries();
			if (enu == null)
				return;
			while (enu.hasMoreElements()) {
				ZipEntry ze = (ZipEntry) enu.nextElement();
				String zeName = ze.getName();
				if (zeName.toLowerCase().contains(fileName.toLowerCase())) {
					list.add(s);
					Log.getLogger().info("Found: " + s.getName());
					break;
				}
			}
		}
		
	}

	/**
	 * Build file.
	 * 
	 * @param f
	 *            the file
	 * @throws IOException
	 */
	protected final void buildFile(File f) throws IOException {
		if (!f.exists()) {
			if (f.isDirectory()) {
				f.mkdirs();
			} else {
				if (f.getParentFile() != null) {
					f.getParentFile().mkdirs();
				}
				try {
					f.createNewFile();
				} catch (Exception e) {
					System.err.println("Can't create file at: "
							+ f.getAbsolutePath());
				}
			}
		}
	}

	/**
	 * Loop to compress
	 * 
	 * @param zos
	 *            zip output stream
	 * @param file
	 *            the file to be write in
	 * @param the
	 *            secret key.
	 * @throws Exception
	 */
	protected final void zipLoop(ZipOutputStream zos, File file) {
		try {
			if (file.isDirectory()) {
				String relativePathTmp = this.relativePath;
				this.relativePath += file.getName() + File.separator;
				this.createZipNode(zos, this.relativePath);
				File[] fs = file.listFiles();
				for (File f : fs)
					this.zipLoop(zos, f);
				this.relativePath = relativePathTmp;
			} else {
				this.zipSingleFile(zos, file);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Compress single file.
	 * 
	 * @param zos
	 *            zip output stream
	 * @param file
	 *            the file to be write in
	 * @throws IOException
	 */
	protected final void zipSingleFile(ZipOutputStream zos, File file)
			throws IOException {

		Log.getLogger().debug("zip: " + this.relativePath + file.getName());
		if (file.getAbsoluteFile().equals(
				new File(this.targetPath).getAbsoluteFile()))
			return;
		ZipEntry entry = new ZipEntry(this.relativePath + file.getName());
		zos.putNextEntry(entry);
		InputStream is = new FileInputStream(file);
		int length = 0;
		byte[] bs = new byte[1 << 10];
		while ((length = is.read(bs)) != -1) {
			zos.write(bs, 0, length);
		}
		zos.flush();
		zos.closeEntry();
		is.close();
	}

	/**
	 * Create directory node in the cmopressed file.
	 * 
	 * @param zos
	 * @param relativePath
	 * @throws IOException
	 */
	protected final void createZipNode(ZipOutputStream zos, String relativePath)
			throws IOException {
		ZipEntry zipEntry = new ZipEntry(relativePath);
		zos.putNextEntry(zipEntry);
		zos.closeEntry();
	}

}
